Skip to content

feat: pre-compress frontend assets at build time#6497

Merged
masenf merged 3 commits into
reflex-dev:mainfrom
FarhanAliRaza:lighthouse-compression
May 20, 2026
Merged

feat: pre-compress frontend assets at build time#6497
masenf merged 3 commits into
reflex-dev:mainfrom
FarhanAliRaza:lighthouse-compression

Conversation

@FarhanAliRaza
Copy link
Copy Markdown
Contributor

@FarhanAliRaza FarhanAliRaza commented May 12, 2026

Generate gzip/brotli/zstd sidecars during reflex export and serve the best match via a new PrecompressedStaticFiles handler. Adds a configurable frontend_compression_formats (defaults to gzip) and switches the prod test harness off the stdlib http.server in favor of the same Starlette/Uvicorn stack used in production so the precompressed path is exercised under integration tests.

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

New Feature Submission:

  • Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?
image

Generate gzip/brotli/zstd sidecars during `reflex export` and serve the
best match via a new PrecompressedStaticFiles handler. Adds a
configurable `frontend_compression_formats` (defaults to gzip) and
switches the prod test harness off the stdlib http.server in favor of
the same Starlette/Uvicorn stack used in production so the precompressed
path is exercised under integration tests.
@FarhanAliRaza FarhanAliRaza requested a review from a team as a code owner May 12, 2026 19:50
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 12, 2026

Merging this PR will not alter performance

βœ… 24 untouched benchmarks


Comparing FarhanAliRaza:lighthouse-compression (64b84a5) with main (5d65aa9)

Open in CodSpeed

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 12, 2026

Greptile Summary

This PR adds build-time pre-compression of frontend static assets (gzip, brotli, zstd) via a new Node.js script (compress-static.js) and a PrecompressedStaticFiles Starlette handler that serves the best sidecar based on Accept-Encoding content negotiation. The test harness (AppHarnessProd) is also upgraded from the stdlib http.server to a real Uvicorn/Starlette server, so integration tests exercise the same stack as production.

  • A new frontend_compression_formats config option (default ["gzip"]) controls which sidecar formats are generated; it is comma-parseable from the environment and validated against a hard set of supported names.
  • PrecompressedStaticFiles extends Starlette's StaticFiles with Accept-Encoding-aware sidecar selection, emits Vary: Accept-Encoding unconditionally when encodings are configured, and re-routes the HTML 404-fallback path through file_response so compressed 404 pages are served correctly.
  • The test harness switch to Uvicorn means precompressed serving is exercised end-to-end in integration tests, which is a meaningful fidelity improvement over the old stdlib server.

Confidence Score: 4/5

Core compression and serving logic is correct; the main risk area is the test harness rewrite, not the feature itself.

The PrecompressedStaticFiles handler and compression pipeline are well-structured and thoroughly tested. The AppHarnessProd rewrite reads uvicorn's internal .servers[0].sockets to discover the bound port and mutates the process-global config.deploy_url β€” neither breaks the feature itself, but both add fragility to the test harness that could surface as confusing failures on a uvicorn upgrade or in long pytest sessions.

reflex/testing.py β€” the uvicorn internals access and global config mutation deserve a second look before this pattern is copied elsewhere in the test suite.

Important Files Changed

Filename Overview
reflex/utils/precompressed_staticfiles.py New PrecompressedStaticFiles handler correctly parses Accept-Encoding, selects the best sidecar by quality value, emits Vary: Accept-Encoding unconditionally when encodings are configured, and re-routes the Starlette 404.html fallback through file_response so it also gets compressed. Logic is sound.
packages/reflex-base/src/reflex_base/.templates/web/compress-static.js Node.js compression script β€” gzip and brotli use maximum quality settings; zstd uses the runtime default (no level specified). HTML files are always compressed regardless of size. Gracefully skips unavailable formats at runtime and rejects them via ensureFormatsSupported.
reflex/testing.py Replaces the stdlib http.server harness with a real Uvicorn/Starlette server, which is a meaningful fidelity improvement. Two concerns: relies on uvicorn's private .servers[0].sockets to discover the OS-assigned port, and mutates the global config singleton's deploy_url field, which can bleed across test modules.
reflex/utils/build.py Adds _compress_static_output invoked after plugins' post_build hooks β€” correct ordering. Runtime selection prefers Node.js over Bun even when Bun is the configured package manager.
reflex/utils/exec.py Replaces StaticFiles with PrecompressedStaticFiles, resolves the static directory path up front, and passes frontend_compression_formats as encodings. Change is minimal and correct.
packages/reflex-base/src/reflex_base/config.py New frontend_compression_formats field defaults to ["gzip"], is parsed from comma-delimited env variable, deduplicates, and validates against a hard set of supported format names. Well-tested.
tests/integration/test_precompressed_frontend.py Integration tests covering compressed index.html and 404 fallback for all three encoding formats. Uses request_raw to avoid client-side decompression, correctly validating magic bytes and headers.
tests/integration/test_prod_build_pipeline.py Verifies sidecar file creation for JS and CSS bundles, and end-to-end content-negotiation against a live prod server. Robust structure with parameterized encoding cases.
tests/integration/utils.py Adds request_raw helper using bare HTTPConnection to bypass automatic decompression β€” appropriate for verifying compressed response bodies directly.

Sequence Diagram

sequenceDiagram
    participant Dev as Developer
    participant Build as build.py
    participant JS as compress-static.js
    participant Server as PrecompressedStaticFiles
    participant Browser as Browser

    Dev->>Build: reflex export
    Build->>JS: node compress-static.js static_dir gzip brotli zstd
    JS->>JS: Walk files matching COMPRESSIBLE_EXTENSIONS
    JS->>JS: Write .gz .br .zst sidecars for each file

    Browser->>Server: GET /assets/app.js with Accept-Encoding br,gzip
    Server->>Server: _parse_accept_encoding header
    Server->>Server: _select_sidecar picks best quality on disk
    Server-->>Browser: 200 Content-Encoding br Vary Accept-Encoding

    Browser->>Server: GET /missing-route
    Server->>Server: super get_response returns FileResponse 404.html
    Server->>Server: Intercept and re-route through file_response
    Server-->>Browser: 404 Content-Encoding br compressed 404.html
Loading

Comments Outside Diff (1)

  1. reflex/utils/build.py, line 411-415 (link)

    P2 Runtime preference may surprise users: Node.js is tried before Bun

    get_node_path() or get_bun_path() picks the system Node.js if it is on PATH, even when Bun is the configured package manager and would also work. compress-static.js uses top-level await and bare import, which require Node.js to be running in ES-module mode. This works today because the .web/package.json carries "type": "module", but the preference order (Node first) means a system with both runtimes present always uses Node even if the project was set up with Bun. Swapping the order to get_bun_path() or get_node_path() would match the rest of the codebase's preference for Bun and avoid the implicit dependency on .web/package.json's "type" field when Bun is available.

Reviews (1): Last reviewed commit: "feat: pre-compress frontend assets at bu..." | Re-trigger Greptile

Comment thread reflex/testing.py
Comment thread reflex/testing.py
@Alek99
Copy link
Copy Markdown
Member

Alek99 commented May 12, 2026

Nice!

Copy link
Copy Markdown
Collaborator

@masenf masenf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some benchies on the docs app: uv run reflex run --env prod

Default compression ["gzip"]

Compiling: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 866/865 0:02:11
Creating Production Build:  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 4/4 0:02:07

["brotli", "zstd"]

Compiling: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 866/865 0:02:14
Creating Production Build:  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 4/4 0:02:12

["brotli"]

Compiling: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 866/865 0:02:30
Creating Production Build:  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 4/4 0:02:22

main

Compiling: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 866/865 0:02:24
Creating Production Build:  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 4/4 0:02:24

Copy link
Copy Markdown
Collaborator

@masenf masenf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good to go for me, but as a follow up PR, it would be nice to tighten up the progress bar. As it is now, a large site that has a lot to compress will just kind of sit there and it's not clear what it's waiting for.

I'll spec out some other tickets, but as part of the opentel stuff, we need to switch over to using python logging and use traces for calculating timing, etc.

@masenf masenf merged commit fc7221a into reflex-dev:main May 20, 2026
70 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants